package simplescenegraph.assets.models.obj;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import simplescenegraph.Material;
import simplescenegraph.PolygonMesh;
import simplescenegraph.Renderer;
import simplescenegraph.assets.FileReader;
import simplescenegraph.assets.Texture;
import simplescenegraph.assets.TextureLoader;
import simplescenegraph.math.Color4f;
import simplescenegraph.math.Vector2f;
import simplescenegraph.math.Vector3f;
import simplescenegraph.utils.StringUtils;

public class OBJModelLoader {
	
	private static final String AMBIENT_COLOR_KEYWORD = "Ka ";

	private static final String SPECULAR_COLOR_KEYWORD = "Ks ";

	private static final String DIFFUSE_COLOR_KEYWORD = "Kd ";

	private static final String TEXTURE_KEYWORD = "map_Kd ";

	private static final String NEW_MATERIAL_KEYWORD = "newmtl ";

	private static final String FACE_KEYWORD = "f ";

	private static final String USE_MATERIAL_KEYWORD = "usemtl ";

	private static final String GROUP_KEYWORD = "g ";

	private static final String MATERIAL_LIBRARY_KEYWORD = "mtllib ";

	private static final String MODEL_NAME_KEYWORD = "o ";

	private static final String UV_COORDINATE_KEYWORD = "vt ";

	private static final String NORMAL_VECTOR_KEYWORD = "vn ";

	private static final String POSITION_KEYWORD = "v ";

	private static final String COMMENT = "# ";
	
	public static OBJModel load(Renderer renderer, String fileName, boolean calculateVertexesNormals) {
		if (renderer == null || fileName == null) {
			throw new IllegalArgumentException();
		}
		
		String parentDir = new File(fileName).getParent();
		
		List<String> lines = null;
		try {
			lines = FileReader.readFileLines(fileName);
		} 
		catch (IOException e) {
			throw new RuntimeException("Error loading OBJ model: '" + fileName + "'", e);
		}
		
		OBJModel model = new OBJModel();
		OBJGroup currentGroup = model.getDefaultGroup();
		String currentMaterial = OBJModel.DEFAULT_MATERIAL_NAME;
		for (String line : lines) {
			if (line.isEmpty() || line.startsWith(COMMENT)) continue;
			
			if (line.startsWith(POSITION_KEYWORD)) {
				model.addPosition(readPosition(line));
			}
			else if (line.startsWith(NORMAL_VECTOR_KEYWORD)) {
				model.addNormal(readNormal(line));
			} 
			else if (line.startsWith(UV_COORDINATE_KEYWORD)) {
				model.addUVCoordinate(readUVCoordinate(line));
			} 
			else if (line.startsWith(MODEL_NAME_KEYWORD)) {
				model.setName(line.substring(2));
			} 
			else if (line.startsWith(MATERIAL_LIBRARY_KEYWORD)) {
				model.addMaterials(readMaterials(renderer, parentDir, line.substring(7)));
			}
			else if (line.startsWith(GROUP_KEYWORD)) {
				currentGroup = newGroup(model, line);
			}
			else if (line.startsWith(USE_MATERIAL_KEYWORD)) {
				currentMaterial = line.substring(7);
				currentGroup.setUseMaterial(currentMaterial);
			} 
			else if (line.startsWith(FACE_KEYWORD)) {
				newFace(currentGroup, line);
			} 
		}
		
		model.prepare(calculateVertexesNormals);
		
		return model;
	}

	private static PolygonMesh newFace(OBJGroup group, String line) {
		if (group == null || line == null) {
			throw new IllegalArgumentException();
		}
		
		OBJFace face = new OBJFace(group);
		String[] columns = line.split("\\s+");
		for (int i = 1; i < columns.length; i++) {
			String column = columns[i];
			
			if (column.indexOf("/") > 0) {
				String[] indexes = column.split("/");

				if (indexes.length > 2) {
					if (indexes[2].length() > 0) {
						face.addNormalIndex(Integer.valueOf(indexes[2]));
					}
					if (indexes[1].length() > 0) {
						face.addUVCoordinateIndex(Integer.valueOf(indexes[1]));
					}
					if (indexes[0].length() > 0) {
						face.addPositionIndex(Integer.valueOf(indexes[0]));
					}
				} 
				else if (indexes.length > 1) {
					if (indexes[1].length() > 0) {
						face.addUVCoordinateIndex(Integer.valueOf(indexes[1]));
					}
					if (indexes[0].length() > 0) {
						face.addPositionIndex(Integer.valueOf(indexes[0]));
					}
				} 
				else if (indexes.length > 0) {
					if (indexes[0].length() > 0) {
						face.addPositionIndex(Integer.valueOf(indexes[0]));
					}
				}
			} 
			else if (column.length() > 0) {
				face.addPositionIndex(Integer.valueOf(column));
			}
		}
		
		return face;
	}

	private static OBJGroup newGroup(OBJModel model, String line) {
		if (model == null || line == null) {
			throw new IllegalArgumentException();
		}
		
		String groupName = line.substring(2);
		OBJGroup newGroup = new OBJGroup(model, groupName);
		
		return newGroup;
	}

	private static Vector2f readUVCoordinate(String line) {
		if (line == null) {
			throw new IllegalArgumentException();
		}
		
		Vector2f uvCoordinate = new Vector2f();
		
		String[] parts = line.split("\\s+");
		uvCoordinate.x = Float.valueOf(parts[1]).floatValue();
		uvCoordinate.y = Float.valueOf(parts[2]).floatValue();
		
		return uvCoordinate;
	}

	private static Vector3f readNormal(String line) {
		if (line == null) {
			throw new IllegalArgumentException();
		}
		
		Vector3f normal = new Vector3f();
		
		String[] parts = line.split("\\s+");
		normal.x = Float.valueOf(parts[1]).floatValue();
		normal.y = Float.valueOf(parts[2]).floatValue();
		normal.z = Float.valueOf(parts[3]).floatValue();
		
		return normal;
	}

	private static Vector3f readPosition(String line) {
		if (line == null) {
			throw new IllegalArgumentException();
		}
		
		Vector3f position = new Vector3f();
		
		String[] parts = line.split("\\s+");
		position.x = Float.valueOf(parts[1]).floatValue();
		position.y = Float.valueOf(parts[2]).floatValue();
		position.z = Float.valueOf(parts[3]).floatValue();
		
		return position;
	}

	private static String getRelativeFilePath(String parentDir, String fileName) {
		if (parentDir == null || fileName == null || fileName.isEmpty()) {
			throw new IllegalArgumentException();
		}
		
		return parentDir + File.separatorChar + fileName;
	}

	private static List<Material> readMaterials(Renderer renderer, String parentDir, String fileName) {
		if (fileName == null) {
			throw new IllegalArgumentException();
		}
		
		List<String> lines;
		try {
			lines = FileReader.readFileLines(getRelativeFilePath(parentDir, fileName));
		} catch (IOException e) {
			throw new RuntimeException("Error reading material library file: '" + fileName + "'", e);
		}
		
		List<Material> materials = new ArrayList<Material>();
		Material currentMaterial = null;
		for (String line : lines) {
			if (line.isEmpty()) continue; 
			
			if (line.startsWith(NEW_MATERIAL_KEYWORD)) {
				currentMaterial = newMaterial(line);
				materials.add(currentMaterial);
			} 
			else if (line.startsWith(TEXTURE_KEYWORD)) {
				Texture texture = readTexture(parentDir, line);
				currentMaterial.setTexture(texture);
				renderer.registerTexture(texture);
			}
			else if (line.startsWith(DIFFUSE_COLOR_KEYWORD)) {
				currentMaterial.setDiffuse(readDiffuseColor(line));
			}
			else if (line.startsWith(SPECULAR_COLOR_KEYWORD)) {
				currentMaterial.setSpecular(readSpecularColor(line));				
			}
			else if (line.startsWith(AMBIENT_COLOR_KEYWORD)) {
				currentMaterial.setAmbient(readAmbientColor(line));
			}
		}
		
		return materials;
	}

	private static Color4f readAmbientColor(String line) {
		if (line == null) {
			throw new IllegalArgumentException();
		}
		
		String parts[] = line.split("\\s+");
		
		Color4f ambient = new Color4f();
		ambient.setRed(Float.valueOf(parts[1]).floatValue());
		ambient.setGreen(Float.valueOf(parts[2]).floatValue());
		ambient.setBlue(Float.valueOf(parts[3]).floatValue());
		
		return ambient;
	}

	private static Color4f readSpecularColor(String line) {
		if (line == null) {
			throw new IllegalArgumentException();
		}
		
		String parts[] = line.split("\\s+");
		
		Color4f specular = new Color4f();
		specular.setRed(Float.valueOf(parts[1]).floatValue());
		specular.setGreen(Float.valueOf(parts[2]).floatValue());
		specular.setBlue(Float.valueOf(parts[3]).floatValue());
		
		return specular;
	}

	private static Color4f readDiffuseColor(String line) {
		if (line == null) {
			throw new IllegalArgumentException();
		}
		
		String parts[] = line.split("\\s+");
		
		Color4f diffuse = new Color4f();
		diffuse.setRed(Float.valueOf(parts[1]).floatValue());
		diffuse.setGreen(Float.valueOf(parts[2]).floatValue());
		diffuse.setBlue(Float.valueOf(parts[3]).floatValue());
		
		return diffuse;
	}

	private static Texture readTexture(String parentDir, String line) {
		if (parentDir == null || line == null) {
			throw new IllegalArgumentException();
		}
		
		String parts[] = line.split("\\s+");
		String textureFileName = StringUtils.join(parts, ' ', 1);
		
		Texture texture;
		try {
			texture = TextureLoader.readTexture(getRelativeFilePath(parentDir, textureFileName));
		} catch (IOException e) {
			throw new RuntimeException("Error loading material texture: '" + textureFileName + "'", e);
		}
		
		return texture;
	}

	private static Material newMaterial(String line) {
		if (line == null) {
			throw new IllegalArgumentException();
		}
		
		Material newMaterial;
		
		String parts[] = line.split("\\s+");
		String materialName = StringUtils.join(parts, ' ', 1);
		newMaterial = new Material(materialName);
		
		return newMaterial;
	}
	
}
